跳到主要内容

Gin 框架学习

Gin 框架是什么?

Gin 是 Go 的 Web 框架,项目地址 Gin

go get -u github.com/gin-gonic/gin

项目各个目录的编排

各个语言项目的目录结构都不一样,记录下这个项目的组织方式:

├─conf        用于存储配置文件
├─middleware 应用中间件
│ └─jwt
├─models 应用数据库模型
├─pkg 存放的是可供项目内部/外部所使用的公共性代码
│ ├─app
│ ├─e
│ ├─export
│ ├─file
│ ├─gredis
│ ├─logging
│ ├─qrcode
│ ├─setting
│ ├─upload
│ └─util
├─routers 路由逻辑处理
│ └─api
│ └─v1
├─runtime 应用运行时数据
│ ├─fonts
│ └─qrcode
├─service
│ ├─article_service
│ ├─auth_service
│ ├─cache_service
│ └─tag_service
└─vendor 第三方包依赖

快速使用

package main

import "github.com/gin-gonic/gin"

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run()
// listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

这个 gin.Default() 返回 Gin 的 type Engine struct{...},里面包含 RouterGroup,相当于创建一个路由 Handlers,可以后期绑定各类的路由规则和函数、中间件等

这个 gin.H{…} 就是一个 map[string]interface{}

这个 gin.Context Context 是 gin 中的上下文,它允许我们在中间件之间传递变量、管理流、验证 JSON 请求、响应 JSON 请求等,在 gin 中包含大量 Context 的方法,例如我们常用的 DefaultQuery、Query、DefaultPostForm、PostForm 等等

访问测试:

curl http://127.0.0.1:8080/ping

Context 的设计

Gin 使用 Context 封装了 Request、Response 这两个对象

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run()
// listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

实际上底层(Gin 包的 gin.go 文件),Gin 内部维护了一个 Context 对象池,用来分配 Context,请求过来时动态的把 req、resp 对象传进去,至于全局污染怎么解决,也简单粗暴,直接在下一个连接过来的时候清空这个对象保存的状态,下图所示:

这个 Context 就是把一堆全局变量打包成一个对象,方便调用它的方法

这个设计在其它的系统中也很常见,可以参考知乎的这个问题 为什么那么多框架都设计有Context?

配置 Gin 框架

配置 Server 服务

func main() {
router := gin.Default()

s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}

这里的 http.ListenAndServe 和上面的 r.Run() 有区别吗?

点进里面,得知本质上没有区别

func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()

address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}

可以直接使用配置

func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}

这个 http.Server

  • Addr:监听的 TCP 地址,格式为:8000
  • Handler:http 句柄,实质为ServeHTTP,用于处理程序响应 HTTP 请求
  • TLSConfig:安全传输层协议(TLS)的配置
  • ReadTimeout:允许读取的最大时间
  • ReadHeaderTimeout:允许读取请求头的最大时间
  • WriteTimeout:允许写入的最大时间
  • IdleTimeout:等待的最大时间
  • MaxHeaderBytes:请求头的最大字节数
  • ConnState:指定一个可选的回调函数,当客户端连接发生变化时调用
  • ErrorLog:指定一个可选的日志记录器,用于接收程序的意外行为和底层系统错误;如果未设置或为 nil 则默认以日志包的标准日志记录器完成(也就是在控制台输出)

控制日志输出颜色

默认情况下,控制台上输出的日志会根据检测到的 TTY 进行着色。

如果不想给日志着色,可以这么做:

func main() {
// 禁止日志颜色
gin.DisableConsoleColor()

// 使用默认中间件(logger 和 recovery)创建 gin 路由器
router := gin.Default()

// 定义路由
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})

// 启动服务器
router.Run(":8080")
}

如果强制给日志着色,可以这么做:

func main() {
// 强制设置日志颜色
gin.ForceConsoleColor()

router := gin.Default()

router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})

router.Run(":8080")
}

自定义日志格式

func main() {
router := gin.New()
// LoggerWithFormatter 中间件会将日志写入 gin.DefaultWriter
// 默认情况下 gin.DefaultWriter 是 os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// 自定义日志输出格式
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
// 使用 recovery 中间件
router.Use(gin.Recovery())
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}

将日志信息写入文件

Gin 框架默认将日志输出到控制台,要写入指定的日志文件,可以这么做

func main() {
// 创建日志文件并设置为 gin.DefaultWriter
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)

// 如果你需要同时写入日志文件和控制台,可以这么做:
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)

router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})

router.Run(":8080")
}
func main() {
router := gin.Default()

router.GET("/cookie", func(c *gin.Context) {
// 读取 Cookie
cookie, err := c.Cookie("gin_cookie")

if err != nil {
cookie = "NotSet"
// 设置 Cookie
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}

fmt.Printf("Cookie value: %s \n", cookie)
})

router.Run(":8088")
}

运行上述代码,访问 /cookie,在控制台日志输出中可以看到打印的 Cookie 信息:

Session

在 Gin 框架中,我们可以依赖 gin-contrib/sessions 中间件处理 session。

go get github.com/gin-contrib/sessions

基本的 session 用法

package main

import (
// 导入session包
"github.com/gin-contrib/sessions"
// 导入session存储引擎
"github.com/gin-contrib/sessions/cookie"
// 导入gin框架包
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()
// 创建基于 cookie 的存储引擎,secret11111 参数是用于加密的密钥
store := cookie.NewStore([]byte("secret11111"))

// 设置 session 中间件,参数 mysession,指的是 session 的名字,也是 cookie 的名字
// store 是前面创建的存储引擎,我们可以替换成其他存储引擎
r.Use(sessions.Sessions("mysession", store))

r.GET("/hello", func(c *gin.Context) {
// 初始化session对象
session := sessions.Default(c)

// 通过 session.Get 读取 session 值
// session是键值对格式数据,因此需要通过key查询数据
if session.Get("hello") != "world" {
// 设置 session 数据
session.Set("hello", "world")
// 删除 session 数据
session.Delete("mytag")
// 保存 session 数据
session.Save()
// 删除整个session
// session.Clear()
}
c.JSON(200, gin.H{"hello": session.Get("hello")})
})

r.Run(":8000")
}

Redis 存储 Session

如果我们想将 Session 数据保存到 Redis 中,只要将 Session 的存储引擎改成 Redis 即可。

go get github.com/gin-contrib/sessions/redis

使用例:

package main

import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()
// 初始化基于redis的存储引擎
// 参数说明:
// 第1个参数 - redis最大的空闲连接数
// 第2个参数 - 数通信协议tcp或者udp
// 第3个参数 - redis地址, 格式,host:port
// 第4个参数 - redis密码
// 第5个参数 - session加密密钥
store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
r.Use(sessions.Sessions("mysession", store))

r.GET("/incr", func(c *gin.Context) {
session := sessions.Default(c)
var count int
v := session.Get("count")
if v == nil {
count = 0
} else {
count = v.(int)
count++
}
session.Set("count", count)
session.Save()
c.JSON(200, gin.H{"count": count})
})
r.Run(":8000")
}

同时启动多个服务

需要下载 Go 官方提供的扩展包 errgroup 可以多子进程

go get -u golang.org/x/sync/errgroup

使用这个拓展实现多个服务

import (
"log"
"net/http"
"time"

"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)

var (
g errgroup.Group
)

func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})

return e
}

func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})

return e
}

func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

g.Go(func() error {
return server01.ListenAndServe()
})

g.Go(func() error {
return server02.ListenAndServe()
})

if err := g.Wait(); err != nil {
log.Fatal(err)
}
}

配置路由

可以抽离出路由这块代码

func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
gin.SetMode(setting.RunMode)

r.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "test",
})
})

return r
}

这样 main 里面就可以直接使用

func main() {
router := InitRouter()
http.ListenAndServe(":8080", router)
}

各种请求类型

// 创建带有默认中间件的路由:
// 日志与恢复中间件
router := gin.Default()
//创建不带中间件的路由:
//r := gin.New()

router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)

路由群组

这个和 SpringMVC 那个配置了 Class 统一路由路径是一样的

func loginEndpoint(c *gin.Context)  {
c.String(200, "login")
}

func submitEndpoint(c *gin.Context) {
c.String(200, "submit")
}

func readEndpoint(c *gin.Context) {
c.String(200, "read")
}

func main() {
router := gin.Default()

// Simple group: v1
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}

// Simple group: v2
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}

router.Run(":8080")
}

这样我们就以版本号为依据设置了 v1、v2 两个分组,运行上述代码,输出结果如下:

定义路由日志格式

Gin 框架默认路由日志格式如下:

[GIN-debug] POST   /foo                      --> main.main.func1 (3 handlers)
[GIN-debug] GET /bar --> main.main.func2 (3 handlers)
[GIN-debug] GET /status --> main.main.func3 (3 handlers)

如果你想要记录指定格式(如 JSON、键值)的信息,可以通过 gin.DebugPrintRouteFunc 来定义这个格式,在下面这个例子中,我们将通过标准日志包记录所有路由信息,你也可以根据需要自定义日志格式:

func main() {
r := gin.Default()

// 默认路由输出格式
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}

r.POST("/foo", func(c *gin.Context) {
c.JSON(http.StatusOK, "foo")
})

r.GET("/bar", func(c *gin.Context) {
c.JSON(http.StatusOK, "bar")
})

r.GET("/status", func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
})

// Listen and Server in http://0.0.0.0:8080
r.Run()
}

启动该服务器,输出结果如下:

接受参数

取得 Get 请求参数

URL 参数通过 DefaultQuery 或 Query 方法获取

// url 为 http://localhost:8080/welcome?name=alsritter
// 输出 Hello alsritter
// url 为 http://localhost:8080/welcome时
// 输出 Hello Guest

router.GET("/welcome", func(c *gin.Context) {
name := c.DefaultQuery("name", "Guest") //可设置默认值
// 是 c.Request.URL.Query().Get("lastname") 的简写
lastname := c.Query("lastname")
fmt.Println("Hello %s", name)
})

RestFul 风格参数

router.GET("/string/:name", func(c *gin.Context) {
name := c.Param("name")
fmt.Printf("Hello %s", name)
})

直接绑定结构体

type Student struct {
ID string `uri:"id" binding:"required,uuid"`
Name string `uri:"name" binding:"required"`
}

func main() {
route := gin.Default()
route.GET("/:name/:id", func(c *gin.Context) {
var student Student
// 将路由参数绑定到结构体中
if err := c.ShouldBindUri(&student); err != nil {
c.JSON(400, gin.H{"msg": err})
return
}
c.JSON(200, gin.H{"name": student.Name, "uuid": student.ID})
})
route.Run(":8088")
}

取得 POST 表单参数

//form
router.POST("/form", func(c *gin.Context) {
// 解析表单字段 type,如果为空的话使用默认值 alert
ty := c.DefaultPostForm("type", "alert")//可设置默认值
msg := c.PostForm("msg")
title := c.PostForm("title")
fmt.Println("type is %s, msg is %s, title is %s", ty, msg, title)
})

或者直接绑定结构体

type Person struct {
Name string `form:"name"`
Address string `form:"address"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}

func startPage(c *gin.Context) {
var person Person
// If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
log.Println(person.Address)
log.Println(person.Birthday)
}

c.String(200, "Success")
}

func main() {
route := gin.Default()
// GET 请求
route.GET("/testing", startPage)
// POST 请求
route.POST("/testing", startPage)
route.Run(":8085")
}

只绑定查询字符串

使用 ShouldBindQuery 方法将只绑定查询字符串(就是 url 里面的参数),而忽略 POST 表单数据:

type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}

func main() {
route := gin.Default()
route.Any("/testing", startPage)
route.Run(":8085")
}

func startPage(c *gin.Context) {
var person Person
if c.ShouldBindQuery(&person) == nil {
log.Println("====== Only Bind By Query String ======")
log.Println(person.Name)
log.Println(person.Address)
}
c.String(200, "Success")
}

嵌套结构体绑定表单

package main

import (
"github.com/gin-gonic/gin"
)

// 最基本的结构体
type StructA struct {
FieldA string `form:"field_a"`
}

// 嵌套结构体的结构体
type StructB struct {
NestedStruct StructA
FieldB string `form:"field_b"`
}

// 嵌套结构体指针的结构体
type StructC struct {
NestedStructPointer *StructA
FieldC string `form:"field_c"`
}

// 嵌套匿名结构体的结构体
type StructD struct {
NestedAnonymousStruct struct {
FieldX string `form:"field_x"`
}
FieldD string `form:"field_d"`
}

// 返回 StructB
func GetDataB(c *gin.Context) {
var b StructB
// 读取请求数据并写入结构体b
c.Bind(&b)
// 返回 JSON 格式响应
c.JSON(200, gin.H{
"a": b.NestedStruct,
"b": b.FieldB,
})
}

// 返回 StructC
func GetDataC(c *gin.Context) {
var b StructC
c.Bind(&b)
c.JSON(200, gin.H{
"a": b.NestedStructPointer,
"c": b.FieldC,
})
}

// 返回 StructD
func GetDataD(c *gin.Context) {
var b StructD
c.Bind(&b)
c.JSON(200, gin.H{
"x": b.NestedAnonymousStruct,
"d": b.FieldD,
})
}

func main() {
r := gin.Default()
r.GET("/getb", GetDataB)
r.GET("/getc", GetDataC)
r.GET("/getd", GetDataD)
r.Run()
}

通过 curl 发起请求:

PureJSON 编码特殊字符

通常,JSON 会通过 Unicode 编码特殊的 HTML 字符,比如将 < 替换成 \u003c,这样一来会影响可读性,如果你想要按照字面意义编码这些字符,可以使用 PureJSON 方法:

func main() {
r := gin.Default()

// Serves unicode entities
r.GET("/json", func(c *gin.Context) {
c.JSON(200, gin.H{
"html": "<b>Hello, world!</b>",
})
})

// Serves literal characters
r.GET("/purejson", func(c *gin.Context) {
c.PureJSON(200, gin.H{
"html": "<b>Hello, world!</b>",
})
})

// listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

运行上述代码,在终端窗口通过 curl 进行测试:

获取请求头

func Index(c *gin.Context) {
ua := c.GetHeader("User-Agent")
// do something ...
}

上传文件

router.POST("/upload", func(c *gin.Context) {
file, header, err := c.Request.FormFile("upload")
filename := header.Filename
fmt.Println(header.Filename)
out, err := os.Create("./tmp/" + filename + ".png")
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
})

响应数据

最简单的响应:

c.String(http.StatusOK, "some string")

响应基本数据类型

JSON/XML/YAML响应:

r.GET("/moreJSON", func(c *gin.Context) {
// You also can use a struct
var msg struct {
Name string `json:"user" xml:"user"`
Message string
Number int
}

msg.Name = "Lena"
msg.Message = "hey"
msg.Number = 123

// 注意 msg.Name 变成了 "user" 字段
// 以下方式都会输出 : {"user": "Lena", "Message": "hey", "Number": 123}
c.JSON(http.StatusOK, gin.H{"user": "Lena", "Message": "hey", "Number": 123})
c.XML(http.StatusOK, gin.H{"user": "Lena", "Message": "hey", "Number": 123})
c.YAML(http.StatusOK, gin.H{"user": "Lena", "Message": "hey", "Number": 123})
c.JSON(http.StatusOK, msg)
c.XML(http.StatusOK, msg)
c.YAML(http.StatusOK, msg)
})

响应视图(HTML 模板)

不推荐这种方式响应页面,因为现在都是前后端分离的,但是某些场景(懒得写前端服务),可以使用这个

先要使用 LoadHTMLTemplates() 方法来加载模板文件

下面展示模板文件传参:

func main() {
router := gin.Default()
//加载模板
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
//定义路由
router.GET("/index", func(c *gin.Context) {
//根据完整文件名渲染模板,并传递参数
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
})
})
router.Run(":8080")
}

模板结构定义

<html>
<h1>
{{ .title }}
</h1>
</html>

不同文件夹下模板名字可以相同,此时需要 LoadHTMLGlob() 加载两层模板路径

router.LoadHTMLGlob("templates/**/*")
router.GET("/posts/index", func(c *gin.Context) {

c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
"title": "Posts",
})

c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
"title": "Users",
})

}

模板文件

<!-- 注意开头 define 与结尾 end 不可少 -->
{{ define "posts/index.tmpl" }}
<html><h1>
{{ .title }}
</h1>
</html>
{{ end }}

重定向

Gin 框架支持内部和外部重定向:

func main()  {
r := gin.Default()

// HTTP 重定向
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com/")
})

// 路由重定向
r.GET("/test1", func(c *gin.Context) {
c.Request.URL.Path = "/test2"
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(200, gin.H{"hello": "world"})
})

r.Run()
}

AsciiJSON 转码

使用 AsciiJSON 方法可以生成只包含 ASCII 字符的 JSON 格式数据,对于非 ASCII 字符会进行转义:

func main()  {
r := gin.Default()

r.GET("/asciiJSON", func(c *gin.Context) {
data := map[string]interface{}{
"lang": "Gin框架",
"tag": "<br/>",
}

// 输出: {"lang":"Gin\u6846\u67b6","tag":"\u003cbr\u003e"}
c.AsciiJSON(http.StatusOK, data)
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

下载文件

func main() {
router := gin.Default()
router.GET("/dataFromReader", func(c *gin.Context) {
response, err := http.Get("https://go.dev/images/go-logo-white.svg")
if err != nil || response.StatusCode != http.StatusOK {
c.Status(http.StatusServiceUnavailable)
return
}

reader := response.Body
contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type")

extraHeaders := map[string]string{
"Content-Disposition": `attachment; filename="go-logo-white.svg"`,
}

c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
})
router.Run(":8088")
}

访问 /dataFromReader 路由会下载图片到本地。

控制器

异步处理请求

goroutine 机制可以方便地实现异步处理,在中间件或处理器中开启新的协程时,不应该在其中使用原生的上下文对象,而应该使用它的只读副本:

func main() {
r := gin.Default()
//1. 异步
r.GET("/long_async", func(c *gin.Context) {
// goroutine 中只能使用只读的上下文 c.Copy()
cCp := c.Copy()
go func() {
time.Sleep(5 * time.Second)

// ⚠️ 注意这里使用的是上下文对象的只读副本 "cCp"
log.Println("Done! in path " + cCp.Request.URL.Path)
}()
})


//2. 同步的写法
r.GET("/long_sync", func(c *gin.Context) {
time.Sleep(5 * time.Second)

// 注意可以使用原始上下文
log.Println("Done! in path " + c.Request.URL.Path)
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

之所以使用副本是为了避免污染其中的数据,context 参数传递的是全局的

使用中间件

func main() {
// 创建一个默认不使用任何中间件的路由器
r := gin.New()

// 设置全局中间件
// Logger 中间件会记录日志到 gin.DefaultWriter,即使你设置了 GIN_MODE=release 这个环境变量
// 默认情况下 gin.DefaultWriter = os.Stdout(控制台标准输出)
r.Use(gin.Logger())

// Recovery 中间件会从任意 panics 中恢复并且返回 500 响应(服务端错误)
r.Use(gin.Recovery())

// 设置路由中间件
// 路由中间件可以被添加到指定路由上,并且不限数量
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

// 为指定路由分组设置中间件
// 认证组
// authorized := r.Group("/", AuthRequired())
// 上面这行代码等同于下面下的代码:
authorized := r.Group("/")
// 下面为 `/` 前缀的路由分组设置中间件 AuthRequired,表示该分组下的路由用户认证后才能访问
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
authorized.POST("/read", readEndpoint)

// 嵌套分组
testing := authorized.Group("testing")
testing.GET("/analytics", analyticsEndpoint)
}

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

注意:

// 这种情况下默认会使用 Logger 和 Recovery 中间件
r := gin.Default()

自定义中间件

自定义一个中间件,通过 c.Next() 分隔请求前和请求后,注意在中间件中开启新的协程时,不应该在其中使用原生的上下文对象,而应该使用它的只读副本:

func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()

// 设置 example 变量
c.Set("example", "12345")

// 请求之前...
c.Next()
// 请求之后...

latency := time.Since(t)
// 打印请求处理时间
log.Print(latency)

// 访问即将发送的响应状态码
status := c.Writer.Status()
log.Println(status)
}
}

func main() {
r := gin.New()
// 使用自定义的 Logger 中间件
r.Use(Logger())

// 定义路由
r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)
// 打印 example 值 12345
log.Println(example)
})

// 监听 0.0.0.0:8080
r.Run(":8080")
}

打印效果

请求类型解析绑定

模型绑定可以将请求体绑定给一个类型,目前支持绑定的类型有 JSON, XML 和标准表单数据 (foo=bar&boo=baz)

要注意的是绑定时需要给字段设置绑定类型的标签 Tag。比如绑定 JSON 数据时,设置 json:"fieldname"。 使用绑定方法时,Gin 会根据请求头中 Content-Type 来自动判断需要解析的类型。

如果明确绑定的类型,可以不用自动推断,而用 BindWith 方法。也可以指定某字段是必需的。如果一个字段被 binding:"required" 修饰而值却是空的,请求会失败并返回错误。

example:

// Binding from JSON
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}

func main() {
router := gin.Default()

// 绑定JSON的例子 ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login

if c.BindJSON(&json) == nil {
if json.User == "manu" && json.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
}
})

// 绑定普通表单的例子 (user=manu&password=123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
// 根据请求头中 content-type 自动推断.
if c.Bind(&form) == nil {
if form.User == "manu" && form.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
}
})

// 绑定多媒体表单的例子 (user=manu&password=123)
router.POST("/login", func(c *gin.Context) {
var form Login
// 可以显式声明来绑定多媒体表单:
// c.BindWith(&form, binding.Form)
// 或者使用自动推断:
if c.Bind(&form) == nil {
if form.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
}
})
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}

表单验证

注册自定义的表单验证器,下面自定义了一个验证器 bookabledate,并且 gtfield=CheckIn 来避免 CheckIn 大于 CheckOut

import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"net/http"
"time"
)


// Booking 中包含了绑定的表单请求字段和验证规则
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}

var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
date, ok := fl.Field().Interface().(time.Time)
if ok {
today := time.Now()
// 如果 date 在当前时间之前则校验失败
if today.After(date) {
return false
}
}
return true
}

func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "预定日期有效!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}

func main() {
route := gin.Default()

// 注册新的自定义验证规则
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}

route.GET("/bookable", getBookable)
route.Run(":8085")
}

启动服务器,测试结果如下:

# check_in 时间在当前时间之前
http://localhost:8085/bookable?check_in=2019-11-11&check_out=2021-11-12
{
"error": "Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"
}

# check_in 时间在 check_out 时间之前
http://localhost:8085/bookable?check_in=2022-11-11&check_out=2021-11-12
{
"error": "Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"
}

http://localhost:8085/bookable?check_in=2022-11-11&check_out=2023-11-12
{
"message": "预定日期有效!"
}

使用 BasicAuth 中间件

Basic Auth 是一种开放平台认证方式,简单的说就是需要你输入用户名和密码才能继续访问。​ 如果需要针对单个路由使用,在要在单路由中注册 BasicAuth 中间件即可。

// 使用BasicAuth中间件
func main(){
engine := gin.Default()
// 设置账号和密码,key:代表账号,value:代表密码
ginAccounts := gin.Accounts{
"user":"password",
"abc":"123",
}

// 注册路由和中间件
engine.GET("/test",gin.BasicAuth(ginAccounts), func(context *gin.Context) {
// 获取中间件BasicAuth
user := context.MustGet(gin.AuthUserKey).(string)
fmt.Println(user)
context.JSON(200,gin.H{"msg":"success"})
})

_ = engine.Run()
}

访问效果

绝大部分情况下,我们都是在路由组中使用 BasicAuth 中间件。

func RunUseBasicAuthWithGroup() {
engine := gin.Default()
// 注册路由组和中间件
userGroup := engine.Group("/user", gin.BasicAuth(gin.Accounts{
"abc": "123",
}))

userGroup.GET("info", func(context *gin.Context) {
context.JSON(200, gin.H{"msg": "user.info"})
})
}

为 Gin 框架编写测试用例

例如编写一个服务

package main

import "github.com/gin-gonic/gin"

func setupRouter() *gin.Engine {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
return r
}

func main() {
r := setupRouter()
r.Run(":8088")
}

对应的测试代码,需要和待测试代码位于同一目录下

package main

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestPingRoute(t *testing.T) {
router := setupRouter()

w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
router.ServeHTTP(w, req)

assert.Equal(t, 200, w.Code)
assert.Equal(t, "pong", w.Body.String())
}

Reference

Gin Web Framework Document Gin框架如何处理session Gin 使用教程 gin牛逼的context Gin框架(九):BasicAuth授权认证中间件使用